测试覆盖率与 CI 集成
测试写好了不代表会持续运行。这节配置覆盖率门禁和 CI 自动化——每次提交自动跑测试,覆盖率不达标就不合并。本节在 9-04 CI/CD 基础上,聚焦组件库测试的 CI 配置和排查;端到端测试的完整实战见下一节。
覆盖率配置
Vitest 内置覆盖率支持,使用 V8 覆盖率引擎(@vitest/coverage-v8,版本与 vitest 本体联动)。ai-chat-ui 的覆盖率配置在根级 vitest.config.ts:
// vitest.config.ts(仓库根目录)coverage 段
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html', 'lcov'],
include: ['packages/markdown/src/**'],
exclude: [
'packages/markdown/src/types/**', // 纯类型文件不统计
],
thresholds: {
statements: 80,
branches: 75,
functions: 80,
lines: 80,
},
},
},
})
typescript
两个设计决策:
- thresholds 门禁:statements、branches、functions、lines 四指标低于阈值时 Vitest 返回非零退出码,CI 判定失败。目前门禁数值(80/75/80/80)只对 markdown 包生效——它是解析重、行为稳定的包,适合做门禁试点;core 与 vue 包靠规则约定目标(见下表),避免为凑数字写弱测试。
- exclude 纯类型文件:
types/**只有类型声明没有运行时行为,统计进去只会稀释覆盖率。
各包的覆盖率目标
不同包有不同的合理目标(规则口径,见 .claude/rules/frontend-testing.md):
| 包 | 目标 | 理由 |
|---|---|---|
| core | 90%+ | 纯逻辑,测试成本低,必须高覆盖 |
| vue | 70%+ | 涉及 DOM,部分交互难以完全覆盖 |
| markdown | 75%+(门禁强制 80/75/80/80) | 解析逻辑可测试,高亮依赖浏览器 |
覆盖率是手段不是 KPI:上一节实测三包行覆盖从 87% 提到 98%,靠的是"三场景 + AAA"的用例质量,不是为凑数复制粘贴。
GitHub Actions 测试 CI 配置
基于 9-04 的通用 CI 配置,这里聚焦测试环节:
# .github/workflows/test.yml
name: Test
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [22, 24] # 对齐仓库 engines: ^22.18.0 || >=24.12.0
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 11 # 对齐仓库 packageManager: pnpm@11.21.0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run tests
run: pnpm test # vitest run,根级单配置一把跑全部包
- name: Check coverage
run: pnpm test:coverage
- name: Upload coverage
if: matrix.node-version == 24
uses: codecov/codecov-action@v5
with:
files: ./coverage/lcov.info
token: ${{ secrets.CODECOV_TOKEN }}
yaml
关键配置说明:
- push + PR 触发:main 分支的 push 和所有 PR 都跑测试
- 矩阵测试:Node.js 22 和 24 两个版本(对齐
engines字段,Node 18 已不在支持范围) - frozen-lockfile:CI 中不允许修改 lock 文件,确保依赖一致性
- Codecov 上传:只在一个矩阵分支上传一次,可视化查看覆盖率变化趋势
Monorepo 测试脚本
真实仓库的测试脚本收敛在根目录 package.json,因为根级单配置的 include 已经覆盖所有包的 __tests__/,一条命令一把跑:
// 根目录 package.json
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui"
}
}
json
早期每包各配一份 vitest.config.ts、用 pnpm -r --parallel run test 递归跑的方案也可行,但 N 份配置会漂移(environment、alias 各改各的)。收敛为根级单配置后:vitest 自己按 CPU 调度文件级并行,不需要 --parallel 递归;E2E 是另一条工具链(Playwright),独立脚本入口。
包之间确需按依赖顺序验证时(core → vue → markdown),用 --filter:
{
"scripts": {
"test:ordered": "pnpm --filter @toimc/core run test && pnpm --filter @toimc/vue run test && pnpm --filter @toimc/markdown run test"
}
}
json
CI 测试失败排查(四步法)
CI 构建失败是最常见的自动化问题——本地好好的,push 上去就挂了。传统方式要在几百行日志里找报错,AI 时代四步搞定。
第 1 步:获取失败日志
# 查看最近失败的 run
gh run list --status failure --limit 3
# 查看某个 run 的失败日志
gh run view <run-id> --log-failed
bash
也可以在 GitHub 网页上查看:Actions 面板 → 点击失败的 run → 查看日志。
第 2 步:把日志喂给 AI
> CI 测试失败了,日志如下:
>
> [粘贴 gh run view --log-failed 的输出]
>
> 请分析失败原因并给出修复步骤。
text
第 3 步:AI 分析常见失败模式
| 错误类型 | 典型报错 | AI 会告诉你 |
|---|---|---|
| lockfile 不一致 | ERR_PNPM_LOCKFILE_MISSING | 忘记提交 pnpm-lock.yaml,先 pnpm install 再提交 |
| TypeScript 错误 | Type error: ... is not assignable | CI 用 strict 模式,你本地 tsconfig 少了 strict |
| 依赖版本冲突 | ERESOLVE unable to resolve | CI 的 Node 版本与本地不同,检查 .nvmrc |
| 测试超时 | Timeout - Async callback was not invoked | CI 环境比本地慢,增加测试超时时间 |
| 内存不足 | FATAL ERROR: Reached heap limit | CI 机器资源有限,设置 NODE_OPTIONS=--max-old-space-size=4096 |
| 覆盖率不达标 | Coverage thresholds not met | 新增代码没有写测试,补测试或调整 thresholds |
以前需要翻文档排查的问题,现在把日志粘贴给 AI,10 秒出答案。
第 4 步:修复并验证
> 根据你的分析,CI 失败是因为 pnpm-lock.yaml 没有提交。
> 请帮我重新安装依赖并提交 lockfile。
text
AI 执行 pnpm install、暂存 lockfile、生成 commit。push 后 CI 绿色勾。
测试特有的失败模式
除了通用 CI 问题,测试 CI 还有这些特有场景:
- 快照不匹配:组件渲染结果变了,需要更新快照(
vitest -u) - 异步时序问题:nextTick 后断言,或用
vi.waitFor - 依赖缺失:jsdom 中缺少 matchMedia、IntersectionObserver 等 API
- 环境差异:CI 环境的时区、语言、Node 版本不同(jsdom 的
navigator.language是 en-US,断言中文文案前要显式切 locale——本仓库集成测试的真实教训)
对于 jsdom 缺失的浏览器 API,在根级 setup 文件统一 polyfill(见 13-01),不在单个测试里打补丁:
// tests/setup.ts(节选)
import '@testing-library/jest-dom/vitest'
if (typeof window !== 'undefined' && !window.matchMedia) {
window.matchMedia = (query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: () => {},
removeListener: () => {},
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => false,
})
}
if (typeof window !== 'undefined' && typeof window.ResizeObserver === 'undefined') {
window.ResizeObserver = class {
observe() {}
unobserve() {}
disconnect() {}
}
}
typescript
预防 CI 失败
本地先跑一遍再 push:
> 请帮我依次运行 lint、build、test,确保全部通过后再提交
text
项目根目录放 .nvmrc 锁定 Node 版本(对齐 engines,如 22)。
Pre-commit Hook 集成
用 simple-git-hooks 在提交前自动检查(真实仓库 pre-commit 跑 lint-staged 格式化 + ESLint 修复 + 文档同步检查):
// package.json
{
"simple-git-hooks": {
"pre-commit": "npx lint-staged"
},
"lint-staged": {
"*.{ts,vue}": ["eslint --fix", "prettier --write"]
}
}
json
全量测试放 pre-commit 会太慢。只跑变更相关测试用 vitest 的 related 模式:
{
"lint-staged": {
"packages/core/src/**/*.ts": ["vitest related --run"],
"packages/vue/src/**/*.vue": ["vitest related --run"]
}
}
json
E2E 不适合放 pre-commit(要起真实浏览器,分钟级),放 CI 的独立 job(下一节)。
小结
- 覆盖率门禁用 thresholds 设置在根级配置:markdown 包强制 80/75/80/80,core 90%+、vue 70%+ 走规则约定
- 测试脚本收敛根目录一把跑(
vitest run),E2E 独立入口(playwright test);需要按依赖顺序时用--filter - GitHub Actions 测试 CI:push + PR 触发,矩阵 Node 22/24(对齐 engines),frozen-lockfile,Codecov 单分支上传
- CI 失败四步排查:获取日志 → 喂给 AI → 分析失败模式 → 修复验证;jsdom 缺失 API 统一 setup polyfill
- Pre-commit 只跑轻量检查与变更相关测试,E2E 留给 CI
下一节端到端测试实战——用 Playwright 测 playground 的真实用户流程,以及用浏览器 MCP 让不写代码的人也能做验证。
↑